import math,random
import pandas as pd
from cy_query.db.models.expressions import Func, Value
from cy_query.db.models.fields import FloatField, IntegerField
from cy_query.db.models.functions import Cast
from cy_query.db.models.functions.mixins import (
    FixDecimalInputMixin,
    NumericOutputFieldMixin,
)
from cy_query.db.models.lookups import Transform


class Cache:
    cache_function = None

    def as_cache(self,cache_queryset):
        field = self.source_expressions[0]
        if len(self.source_expressions) == 1:
            return cache_queryset._result_cache[field.name].apply(lambda x:self.cache_function(x) if not pd.isna(x) else None)
        args = [i.as_cache(cache_queryset) for i in self.source_expressions[1:]]
        return cache_queryset._result_cache[field.name].apply(lambda x: self.cache_function(x,*args) if not pd.isna(x) else None)


class Abs(Cache,Transform):
    function = "ABS"
    lookup_name = "abs"
    cache_function = abs


class ACos(Cache,NumericOutputFieldMixin, Transform):
    function = "ACOS"
    lookup_name = "acos"
    cache_function = math.acos


class ASin(Cache,NumericOutputFieldMixin, Transform):
    function = "ASIN"
    lookup_name = "asin"
    cache_function = math.asin


class ATan(Cache,NumericOutputFieldMixin, Transform):
    function = "ATAN"
    lookup_name = "atan"
    cache_function = math.atan


class ATan2(Cache,NumericOutputFieldMixin, Func):
    function = "ATAN2"
    arity = 2
    cache_function = math.atan2

    def as_sqlite(self, compiler, connection, **extra_context):
        if not getattr(
            connection.ops, "spatialite", False
        ) or connection.ops.spatial_version >= (5, 0, 0):
            return self.as_sql(compiler, connection)
        # This function is usually ATan2(y, x), returning the inverse tangent
        # of y / x, but it's ATan2(x, y) on SpatiaLite < 5.0.0.
        # Cast integers to float to avoid inconsistent/buggy behavior if the
        # arguments are mixed between integer and float or decimal.
        # https://www.gaia-gis.it/fossil/libspatialite/tktview?name=0f72cca3a2
        clone = self.copy()
        clone.set_source_expressions(
            [
                Cast(expression, FloatField())
                if isinstance(expression.output_field, IntegerField)
                else expression
                for expression in self.get_source_expressions()[::-1]
            ]
        )
        return clone.as_sql(compiler, connection, **extra_context)


class Ceil(Cache,Transform):
    function = "CEILING"
    lookup_name = "ceil"
    cache_function = math.ceil

    def as_oracle(self, compiler, connection, **extra_context):
        return super().as_sql(compiler, connection, function="CEIL", **extra_context)


class Cos(Cache,NumericOutputFieldMixin, Transform):
    function = "COS"
    lookup_name = "cos"
    cache_function = math.cos


class Cot(Cache,NumericOutputFieldMixin, Transform):
    function = "COT"
    lookup_name = "cot"

    def as_oracle(self, compiler, connection, **extra_context):
        return super().as_sql(
            compiler, connection, template="(1 / TAN(%(expressions)s))", **extra_context
        )

    def as_cache(self,cache_queryset):
        raise NotImplementedError('COT is not supported in cache')


class Degrees(Cache,NumericOutputFieldMixin, Transform):
    function = "DEGREES"
    lookup_name = "degrees"
    cache_function = math.degrees

    def as_oracle(self, compiler, connection, **extra_context):
        return super().as_sql(
            compiler,
            connection,
            template="((%%(expressions)s) * 180 / %s)" % math.pi,
            **extra_context,
        )


class Exp(Cache,NumericOutputFieldMixin, Transform):
    function = "EXP"
    lookup_name = "exp"
    cache_function = math.exp


class Floor(Cache,Transform):
    function = "FLOOR"
    lookup_name = "floor"
    cache_function = math.floor


class Ln(Cache,NumericOutputFieldMixin, Transform):
    function = "LN"
    lookup_name = "ln"

    def as_cache(self,cache_queryset):
        raise NotImplementedError('LN is not supported in cache')


class Log(Cache,FixDecimalInputMixin, NumericOutputFieldMixin, Func):
    function = "LOG"
    arity = 2
    cache_function = math.log

    def as_sqlite(self, compiler, connection, **extra_context):
        if not getattr(connection.ops, "spatialite", False):
            return self.as_sql(compiler, connection)
        # This function is usually Log(b, x) returning the logarithm of x to
        # the base b, but on SpatiaLite it's Log(x, b).
        clone = self.copy()
        clone.set_source_expressions(self.get_source_expressions()[::-1])
        return clone.as_sql(compiler, connection, **extra_context)


class Mod(Cache,FixDecimalInputMixin, NumericOutputFieldMixin, Func):
    function = "MOD"
    arity = 2

    def as_cache(self,cache_queryset):
        raise NotImplementedError('MOD is not supported in cache')


class Pi(Cache,NumericOutputFieldMixin, Func):
    function = "PI"
    arity = 0
    cache_function = math.pi

    def as_oracle(self, compiler, connection, **extra_context):
        return super().as_sql(
            compiler, connection, template=str(math.pi), **extra_context
        )

    def as_cache(self,cache_queryset):
        return pd.Series(self.cache_function, index=cache_queryset._result_cache.index)


class Power(Cache,NumericOutputFieldMixin, Func):
    function = "POWER"
    arity = 2
    cache_function = math.pow


class Radians(Cache,NumericOutputFieldMixin, Transform):
    function = "RADIANS"
    lookup_name = "radians"
    cache_function = math.radians

    def as_oracle(self, compiler, connection, **extra_context):
        return super().as_sql(
            compiler,
            connection,
            template="((%%(expressions)s) * %s / 180)" % math.pi,
            **extra_context,
        )


class Random(Cache,NumericOutputFieldMixin, Func):
    function = "RANDOM"
    arity = 0
    cache_function = random.random

    def as_mysql(self, compiler, connection, **extra_context):
        return super().as_sql(compiler, connection, function="RAND", **extra_context)

    def as_oracle(self, compiler, connection, **extra_context):
        return super().as_sql(
            compiler, connection, function="DBMS_RANDOM.VALUE", **extra_context
        )

    def as_sqlite(self, compiler, connection, **extra_context):
        return super().as_sql(compiler, connection, function="RAND", **extra_context)

    def get_group_by_cols(self):
        return []

    def as_cache(self,cache_queryset):
        return pd.Series(self.cache_function(), index=cache_queryset._result_cache.index)


class Round(Cache,FixDecimalInputMixin, Transform):
    function = "ROUND"
    lookup_name = "round"
    cache_function = round
    arity = None  # Override Transform's arity=1 to enable passing precision.

    def __init__(self, expression, precision=0, **extra):
        super().__init__(expression, precision, **extra)

    def as_sqlite(self, compiler, connection, **extra_context):
        precision = self.get_source_expressions()[1]
        if isinstance(precision, Value) and precision.value < 0:
            raise ValueError("SQLite does not support negative precision.")
        return super().as_sqlite(compiler, connection, **extra_context)

    def _resolve_output_field(self):
        source = self.get_source_expressions()[0]
        return source.output_field


class Sign(Cache,Transform):
    function = "SIGN"
    lookup_name = "sign"

    def as_cache(self,cache_queryset):
        raise NotImplementedError('SIGN is not supported in cache')


class Sin(Cache,NumericOutputFieldMixin, Transform):
    function = "SIN"
    lookup_name = "sin"
    cache_function = math.sin


class Sqrt(Cache,NumericOutputFieldMixin, Transform):
    function = "SQRT"
    lookup_name = "sqrt"
    cache_function = math.sqrt


class Tan(Cache,NumericOutputFieldMixin, Transform):
    function = "TAN"
    lookup_name = "tan"
    cache_function = math.tan
